home *** CD-ROM | disk | FTP | other *** search
/ PC World 2008 September / PCWorld_2008-09_cd.bin / v cisle / sadanastroju / lightning-0.8-tb-win.xpi / js / calUtils.js < prev    next >
Text File  |  2008-03-02  |  85KB  |  2,253 lines

  1. /* ***** BEGIN LICENSE BLOCK *****
  2.  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  3.  *
  4.  * The contents of this file are subject to the Mozilla Public License Version
  5.  * 1.1 (the "License"); you may not use this file except in compliance with
  6.  * the License. You may obtain a copy of the License at
  7.  * http://www.mozilla.org/MPL/
  8.  *
  9.  * Software distributed under the License is distributed on an "AS IS" basis,
  10.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  11.  * for the specific language governing rights and limitations under the
  12.  * License.
  13.  *
  14.  * The Original Code is Calendar component utils.
  15.  *
  16.  * The Initial Developer of the Original Code is
  17.  *   Joey Minta <jminta@gmail.com>
  18.  * Portions created by the Initial Developer are Copyright (C) 2006
  19.  * the Initial Developer. All Rights Reserved.
  20.  *
  21.  * Contributor(s):
  22.  *   Philipp Kewisch <mozilla@kewis.ch>
  23.  *   Daniel Boelzle <daniel.boelzle@sun.com>
  24.  *
  25.  * Alternatively, the contents of this file may be used under the terms of
  26.  * either the GNU General Public License Version 2 or later (the "GPL"), or
  27.  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  28.  * in which case the provisions of the GPL or the LGPL are applicable instead
  29.  * of those above. If you wish to allow use of your version of this file only
  30.  * under the terms of either the GPL or the LGPL, and not to allow others to
  31.  * use your version of this file under the terms of the MPL, indicate your
  32.  * decision by deleting the provisions above and replace them with the notice
  33.  * and other provisions required by the GPL or the LGPL. If you do not delete
  34.  * the provisions above, a recipient may use your version of this file under
  35.  * the terms of any one of the MPL, the GPL or the LGPL.
  36.  *
  37.  * ***** END LICENSE BLOCK ***** */
  38.  
  39. /* This file contains commonly used functions in a centralized place so that
  40.  * various components (and other js scopes) don't need to replicate them. Note
  41.  * that loading this file twice in the same scope will throw errors.
  42.  */
  43.  
  44. /* Returns a clean new calIEvent */
  45. function createEvent() {
  46.     return Components.classes["@mozilla.org/calendar/event;1"].
  47.            createInstance(Components.interfaces.calIEvent);
  48. }
  49.  
  50. /* Returns a clean new calITodo */
  51. function createTodo() {
  52.     return Components.classes["@mozilla.org/calendar/todo;1"].
  53.            createInstance(Components.interfaces.calITodo);
  54. }
  55.  
  56. /* Returns a clean new calIDateTime */
  57. function createDateTime() {
  58.     return Components.classes["@mozilla.org/calendar/datetime;1"].
  59.            createInstance(Components.interfaces.calIDateTime);
  60. }
  61.  
  62. /* Returns a clean new calIRecurrenceInfo */
  63. function createRecurrenceInfo(aItem) {
  64.     var recInfo = Components.classes["@mozilla.org/calendar/recurrence-info;1"].
  65.            createInstance(Components.interfaces.calIRecurrenceInfo);
  66.     recInfo.item = aItem;
  67.     return recInfo;
  68. }
  69.  
  70. /* Returns a clean new calIRecurrenceRule */
  71. function createRecurrenceRule() {
  72.     return Components.classes["@mozilla.org/calendar/recurrence-rule;1"].
  73.            createInstance(Components.interfaces.calIRecurrenceRule);
  74. }
  75.  
  76. /* Returns a clean new calIAttendee */
  77. function createAttendee() {
  78.     return Components.classes["@mozilla.org/calendar/attendee;1"].
  79.            createInstance(Components.interfaces.calIAttendee);
  80. }
  81.  
  82. /* Shortcut to the io service */
  83. function getIOService() {
  84.     if (getIOService.mObject === undefined) {
  85.         getIOService.mObject = Components.classes["@mozilla.org/network/io-service;1"]
  86.                                          .getService(Components.interfaces.nsIIOService2);
  87.     }
  88.     return getIOService.mObject;
  89. }
  90.  
  91. /* Shortcut to the calendar-manager service */
  92. function getCalendarManager() {
  93.     if (getCalendarManager.mObject === undefined) {
  94.         getCalendarManager.mObject = Components.classes["@mozilla.org/calendar/manager;1"]
  95.                                                .getService(Components.interfaces.calICalendarManager);
  96.     }
  97.     return getCalendarManager.mObject;
  98. }
  99.  
  100. /* Shortcut to the ICS service */
  101. function getIcsService() {
  102.     if (getIcsService.mObject === undefined) {
  103.         getIcsService.mObject = Components.classes["@mozilla.org/calendar/ics-service;1"]
  104.                                           .getService(Components.interfaces.calIICSService);
  105.     }
  106.     return getIcsService.mObject;
  107. }
  108.  
  109. /* Shortcut to the timezone service */
  110. function getTimezoneService() {
  111.     if (getTimezoneService.mObject === undefined) {
  112.         getTimezoneService.mObject = Components.classes["@mozilla.org/calendar/timezone-service;1"]
  113.                                                .getService(Components.interfaces.calITimezoneService);
  114.     }
  115.     return getTimezoneService.mObject;
  116. }
  117.  
  118. /* Shortcut to calendar search service */
  119. function getCalendarSearchService() {
  120.     if (getCalendarSearchService.mObject === undefined) {
  121.         getCalendarSearchService.mObject = Components.classes["@mozilla.org/calendar/calendarsearch-service;1"]
  122.                                                      .getService(Components.interfaces.calICalendarSearchProvider);
  123.     }
  124.     return getCalendarSearchService.mObject;
  125. }
  126.  
  127. /// @return the UTC timezone.
  128. function UTC() {
  129.     if (UTC.mObject === undefined) {
  130.         UTC.mObject = getTimezoneService().UTC;
  131.     }
  132.     return UTC.mObject;
  133. }
  134.  
  135. /// @return the floating timezone.
  136. function floating() {
  137.     if (floating.mObject === undefined) {
  138.         floating.mObject = getTimezoneService().floating;
  139.     }
  140.     return floating.mObject;
  141. }
  142.  
  143. /**
  144.  * Function to get the (cached) best guess at a user's default timezone.  We'll
  145.  * use the value of the calendar.timezone.local preference, if it exists.  If
  146.  * not, we'll do our best guess.
  147.  *
  148.  * @return user's default timezone.
  149.  */
  150. function calendarDefaultTimezone() {
  151.     if (calendarDefaultTimezone.mTz === undefined) {
  152.         var prefTzid = getPrefSafe("calendar.timezone.local", null);
  153.         var tzid = prefTzid;
  154.         if (!tzid) {
  155.             tzid = guessSystemTimezone();
  156.         }
  157.         calendarDefaultTimezone.mTz = getTimezoneService().getTimezone(tzid);
  158.         ASSERT(calendarDefaultTimezone.mTz, "timezone not found: " + tzid);
  159.         // Update prefs if necessary:
  160.         if (calendarDefaultTimezone.mTz && calendarDefaultTimezone.mTz.tzid != prefTzid) {
  161.             setPref("calendar.timezone.local", "CHAR", calendarDefaultTimezone.mTz.tzid);
  162.         }
  163.     }
  164.     return calendarDefaultTimezone.mTz;
  165. }
  166.  
  167. /**
  168.  * Format the given string to work inside a CSS rule selector
  169.  * (and as part of a non-unicode preference key).
  170.  *
  171.  * Replaces each space ' ' char with '_'.
  172.  * Replaces each char other than ascii digits and letters, with '-uxHHH-'
  173.  * where HHH is unicode in hexadecimal (variable length, terminated by the '-').
  174.  *
  175.  * Ensures: result only contains ascii digits, letters,'-', and '_'.
  176.  * Ensures: result is invertible, so (f(a) = f(b)) implies (a = b).
  177.  *   also means f is not idempotent, so (a != f(a)) implies (f(a) != f(f(a))).
  178.  * Ensures: result must be lowercase.
  179.  * Rationale: preference keys require 8bit chars, and ascii chars are legible
  180.  *              in most fonts (in case user edits PROFILE/prefs.js).
  181.  *            CSS class names in Gecko 1.8 seem to require lowercase,
  182.  *              no punctuation, and of course no spaces.
  183.  *   nmchar        [_a-zA-Z0-9-]|{nonascii}|{escape}
  184.  *   name        {nmchar}+
  185.  *   http://www.w3.org/TR/CSS21/grammar.html#scanner
  186.  *
  187.  * @param aString       The unicode string to format
  188.  * @return              The formatted string using only chars [_a-zA-Z0-9-]
  189.  */
  190. function formatStringForCSSRule(aString) {
  191.     function toReplacement(ch) {
  192.         // char code is natural number (positive integer)
  193.         var nat = ch.charCodeAt(0);
  194.         switch(nat) {
  195.             case 0x20: // space
  196.                 return "_";
  197.             default:
  198.                 return "-ux" + nat.toString(16) + "-"; // lowercase
  199.         }
  200.     }
  201.     // Result must be lowercase or style rule will not work.
  202.     return aString.toLowerCase().replace(/[^a-zA-Z0-9]/g, toReplacement);
  203. }
  204.  
  205. /**
  206.  * We're going to do everything in our power, short of rumaging through the
  207.  * user's actual file-system, to figure out the time-zone they're in.  The
  208.  * deciding factors are the offsets given by (northern-hemisphere) summer and
  209.  * winter JSdates.  However, when available, we also use the name of the
  210.  * timezone in the JSdate, or a string-bundle term from the locale.
  211.  *
  212.  * @return a mozilla ICS timezone string.
  213. */
  214. function guessSystemTimezone() {
  215.     // Probe JSDates for basic OS timezone offsets and names.
  216.     const dateJun = (new Date(2005, 5,20)).toString();
  217.     const dateDec = (new Date(2005,11,20)).toString();
  218.     const tzNameRegex = /[^(]* ([^ ]*) \(([^)]+)\)/;
  219.     const nameDataJun = dateJun.match(tzNameRegex);
  220.     const nameDataDec = dateDec.match(tzNameRegex);
  221.     const tzNameJun = nameDataJun && nameDataJun[2];
  222.     const tzNameDec = nameDataDec && nameDataDec[2];
  223.     const offsetRegex = /[+-]\d{4}/;
  224.     const offsetJun = dateJun.match(offsetRegex)[0];
  225.     const offsetDec = dateDec.match(offsetRegex)[0];
  226.  
  227.     const tzSvc = getTimezoneService();
  228.  
  229.     function getIcalString(component, property) {
  230.         return (component &&
  231.                 component.getFirstProperty(property).valueAsIcalString);
  232.     }
  233.  
  234.     // Check if Olson ZoneInfo timezone matches OS/JSDate timezone properties:
  235.     // * standard offset and daylight/summer offset if present (longitude),
  236.     // * if has summer time, direction of change (northern/southern hemisphere)
  237.     // * if has summer time, dates of next transitions
  238.     // * timezone name (such as "Western European Standard Time").
  239.     // Score is 3 if matches dates and names, 2 if matches dates without names,
  240.     // 1 if matches dates within a week (so changes on different weekday),
  241.     // otherwise 0 if no match.
  242.     function checkTZ(tzId) {
  243.         var tz = tzSvc.getTimezone(tzId);
  244.  
  245.         // Have to handle UTC separately because it has no .component.
  246.         if (tz.isUTC) {
  247.             if (offsetDec == 0 && offsetJun == 0) {
  248.                 if (tzNameJun == "UTC" && tzNameDec == "UTC") {
  249.                     return 3;
  250.                 } else {
  251.                     return 2;
  252.                 }
  253.             } else {
  254.                 return 0;
  255.             }
  256.         }
  257.         
  258.         var subComp = tz.component;
  259.         // find currently applicable time period, not just first,
  260.         // because offsets of timezone may be changed over the years.
  261.         var standard = findCurrentTimePeriod(tz, subComp, "STANDARD");
  262.         var standardTZOffset = getIcalString(standard, "TZOFFSETTO");
  263.         var standardName     = getIcalString(standard, "TZNAME");
  264.         var daylight = findCurrentTimePeriod(tz, subComp, "DAYLIGHT");
  265.         var daylightTZOffset = getIcalString(daylight, "TZOFFSETTO");
  266.         var daylightName     = getIcalString(daylight, "TZNAME");
  267.  
  268.         // Try northern hemisphere cases.
  269.         if (offsetDec == standardTZOffset && offsetDec == offsetJun &&
  270.             !daylight) {
  271.             if (standardName && standardName == tzNameJun) {
  272.                 return 3;
  273.             } else {
  274.                 return 2;
  275.             }
  276.         }
  277.  
  278.         if (offsetDec == standardTZOffset && offsetJun == daylightTZOffset &&
  279.             daylight) {
  280.             var dateMatchWt = systemTZMatchesTimeShiftDates(tz, subComp);
  281.             if (dateMatchWt > 0) { 
  282.                 if (standardName && standardName == tzNameJun &&
  283.                     daylightName && daylightName == tzNameDec) {
  284.                     return 3;
  285.                 } else {
  286.                     return dateMatchWt;
  287.                 }
  288.             }
  289.         }
  290.  
  291.         // Now flip them and check again, to cover southern hemisphere cases.
  292.         if (offsetJun == standardTZOffset && offsetDec == offsetJun &&
  293.             !daylight) {
  294.             if (standardName && standardName == tzNameDec) {
  295.                 return 3;
  296.             } else {
  297.                 return 2;
  298.             }
  299.         }
  300.  
  301.         if (offsetJun == standardTZOffset && offsetDec == daylightTZOffset &&
  302.             daylight) {
  303.             var dateMatchWt = systemTZMatchesTimeShiftDates(tz, subComp);
  304.             if (dateMatchWt > 0) { 
  305.                 if (standardName && standardName == tzNameJun &&
  306.                     daylightName && daylightName == tzNameDec) {
  307.                     return 3;
  308.                 } else {
  309.                     return dateMatchWt;
  310.                 }
  311.             }
  312.         }
  313.         return 0;
  314.     }
  315.  
  316.     // returns 2=match-within-hours, 1=match-within-week, 0=no-match
  317.     function systemTZMatchesTimeShiftDates(tz, subComp) {
  318.         // Verify local autumn and spring shifts also occur in system timezone
  319.         // (jsDate) on correct date in correct direction.
  320.         // (Differs for northern/southern hemisphere.
  321.         //  Local autumn shift is to local winter STANDARD time.
  322.         //  Local spring shift is to local summer DAYLIGHT time.)
  323.         const autumnShiftJSDate =
  324.             findCurrentTimePeriod(tz, subComp, "STANDARD", true);
  325.         const afterAutumnShiftJSDate = new Date(autumnShiftJSDate);
  326.         const beforeAutumnShiftJSDate = new Date(autumnShiftJSDate);
  327.         const springShiftJSDate =
  328.             findCurrentTimePeriod(tz, subComp, "DAYLIGHT", true);
  329.         const beforeSpringShiftJSDate = new Date(springShiftJSDate);
  330.         const afterSpringShiftJSDate = new Date(springShiftJSDate);
  331.         // Try with 6 HOURS fuzz in either direction, since OS and ZoneInfo
  332.         // may disagree on the exact time of shift (midnight, 2am, 4am, etc).
  333.         beforeAutumnShiftJSDate.setHours(autumnShiftJSDate.getHours()-6);
  334.         afterAutumnShiftJSDate.setHours(autumnShiftJSDate.getHours()+6);
  335.         afterSpringShiftJSDate.setHours(afterSpringShiftJSDate.getHours()+6);
  336.         beforeSpringShiftJSDate.setHours(beforeSpringShiftJSDate.getHours()-6);
  337.         if ((beforeAutumnShiftJSDate.getTimezoneOffset() <
  338.              afterAutumnShiftJSDate.getTimezoneOffset()) &&
  339.             (beforeSpringShiftJSDate.getTimezoneOffset() >
  340.              afterSpringShiftJSDate.getTimezoneOffset())) {
  341.             return 2;
  342.         }          
  343.         // Try with 7 DAYS fuzz in either direction, so if no other tz found,
  344.         // will have a nearby tz that disagrees only on the weekday of shift
  345.         // (sunday vs. friday vs. calendar day), or off by exactly one week,
  346.         // (e.g., needed to guess Africa/Cairo on w2k in 2006).
  347.         beforeAutumnShiftJSDate.setDate(autumnShiftJSDate.getDate()-7);
  348.         afterAutumnShiftJSDate.setDate(autumnShiftJSDate.getDate()+7);
  349.         afterSpringShiftJSDate.setDate(afterSpringShiftJSDate.getDate()+7);
  350.         beforeSpringShiftJSDate.setDate(beforeSpringShiftJSDate.getDate()-7);
  351.         if ((beforeAutumnShiftJSDate.getTimezoneOffset() <
  352.              afterAutumnShiftJSDate.getTimezoneOffset()) &&
  353.             (beforeSpringShiftJSDate.getTimezoneOffset() >
  354.              afterSpringShiftJSDate.getTimezoneOffset())) {
  355.             return 1;
  356.         }
  357.         // no match
  358.         return 0;
  359.     }
  360.  
  361.     const todayUTC = createDateTime(); todayUTC.jsDate = new Date();
  362.     const oneYrUTC = todayUTC.clone(); oneYrUTC.year += 1;
  363.     const periodStartCalDate = createDateTime();
  364.     const periodUntilCalDate = createDateTime(); // until timezone is UTC
  365.     const periodCalRule =
  366.         Components.classes["@mozilla.org/calendar/recurrence-rule;1"]
  367.                   .createInstance(Components.interfaces.calIRecurrenceRule);
  368.     const untilRegex = /UNTIL=(\d{8}T\d{6}Z)/;
  369.  
  370.     function findCurrentTimePeriod(tz, subComp, standardOrDaylight,
  371.                                    isForNextTransitionDate) { 
  372.         periodStartCalDate.timezone = tz;
  373.         // Iterate through 'STANDARD' declarations or 'DAYLIGHT' declarations
  374.         // (periods in history with different settings.
  375.         //  e.g., US changes daylight start in 2007 (from April to March).)
  376.         // Each period is marked by a DTSTART.
  377.         // Find the currently applicable period: has most recent DTSTART
  378.         // not later than today and no UNTIL, or UNTIL is greater than today.
  379.         for (var period = subComp.getFirstSubcomponent(standardOrDaylight);
  380.              period;
  381.              period = subComp.getNextSubcomponent(standardOrDaylight)) {
  382.             periodStartCalDate.icalString = getIcalString(period, "DTSTART");
  383.             if (oneYrUTC.nativeTime < periodStartCalDate.nativeTime) {
  384.                 continue; // period starts too far in future
  385.             }
  386.             // Must examine UNTIL date (not next daylight start) because
  387.             // some zones (e.g., Arizona, Hawaii) may stop using daylight
  388.             // time, so there might not be a next daylight start.
  389.             var rrule = period.getFirstProperty("RRULE");
  390.             if (rrule) { 
  391.                 var match = untilRegex.exec(rrule.valueAsIcalString);
  392.                 if (match) {
  393.                     periodUntilCalDate.icalString = match[1];
  394.                     if (todayUTC.nativeTime > periodUntilDate.nativeTime) {
  395.                         continue; // period ends too early
  396.                     }
  397.                 } // else forever rule
  398.             } // else no daylight rule
  399.  
  400.             // found period that covers today.
  401.             if (!isForNextTransitionDate) {
  402.                 return period;
  403.             } else /*isForNextTranstionDate*/ { 
  404.                 if (todayUTC.nativeTime < periodStartCalDate.nativeTime) {
  405.                     // already know periodStartCalDate < oneYr from now,
  406.                     // and transitions are at most once per year, so it is next.
  407.                     return periodStartCalDate.jsDate;
  408.                 } else if (rrule) { 
  409.                     // find next occurrence after today
  410.                     periodCalRule.icalProperty = rrule;
  411.                     var nextTransitionDate =
  412.                         periodCalRule.getNextOccurrence(periodStartCalDate,
  413.                                                         todayUTC);
  414.                     // make sure rule doesn't end before next transition date.
  415.                     if (nextTransitionDate)
  416.                         return nextTransitionDate.jsDate;
  417.                 }
  418.             }
  419.         }
  420.         // no such period found
  421.         return null; 
  422.     }
  423.  
  424.  
  425.     // Try to find a tz that matches OS/JSDate timezone.  If no name match,
  426.     // will use first of probable timezone(s) with highest score.
  427.     var probableTZId = "floating"; // default fallback tz if no tz matches.
  428.     var probableTZScore = 0;
  429.     var probableTZSource = null;
  430.  
  431.     const sbSvc = 
  432.         Components.classes["@mozilla.org/intl/stringbundle;1"]
  433.         .getService(Components.interfaces.nsIStringBundleService);
  434.     const calProperties =
  435.         sbSvc.createBundle("chrome://calendar/locale/calendar.properties");
  436.  
  437.     // First, try to detect operating system timezone.
  438.     try { 
  439.         var osUserTimeZone = null;
  440.         var zoneInfoIdFromOSUserTimeZone = null;
  441.  
  442.         if (navigator.oscpu.match(/^Windows/)) {
  443.             var regOSName, fileOSName;
  444.             if (navigator.oscpu.match(/^Windows NT/)) {
  445.                 regOSName  = "Windows NT";
  446.                 fileOSName = "WindowsNT";
  447.             } else {
  448.                 // Note: windows 98 compatibility will be deleted
  449.                 // in releases built on Gecko 1.9 or later.
  450.                 regOSName  = "Windows";
  451.                 fileOSName = "Windows98";
  452.             }                    
  453.  
  454.             // If on Windows NT (2K/XP/Vista), current timezone only lists its
  455.             // localized name, so to find its registry key name, match localized
  456.             // name to localized names of each windows timezone listed in
  457.             // registry.  Then use the registry key name to see if this
  458.             // timezone has a known ZoneInfo name.
  459.             var wrk = (Components
  460.                        .classes["@mozilla.org/windows-registry-key;1"]
  461.                        .createInstance(Components.interfaces.nsIWindowsRegKey));
  462.             wrk.open(wrk.ROOT_KEY_LOCAL_MACHINE,
  463.                      "SYSTEM\\CurrentControlSet\\Control\\TimeZoneInformation",
  464.                      wrk.ACCESS_READ);
  465.             var currentTZStandardName = wrk.readStringValue("StandardName");
  466.             wrk.close()
  467.  
  468.             wrk.open(wrk.ROOT_KEY_LOCAL_MACHINE,
  469.                      ("SOFTWARE\\Microsoft\\"+regOSName+
  470.                       "\\CurrentVersion\\Time Zones"),
  471.                      wrk.ACCESS_READ);
  472.  
  473.             // Linear search matching localized name of standard timezone
  474.             // to find the non-localized registry key.
  475.             // (Registry keys are sorted by subkeyName, not by localized name
  476.             //  nor offset, so cannot use binary search.)
  477.             for (var i = 0; i < wrk.childCount; i++) {
  478.               var subkeyName  = wrk.getChildName(i);
  479.               var subkey = wrk.openChild(subkeyName, wrk.ACCESS_READ);
  480.               var std = subkey.readStringValue("Std");
  481.               subkey.close();
  482.               if (std == currentTZStandardName) {
  483.                 osUserTimeZone = subkeyName;
  484.                 break;
  485.               }
  486.             }
  487.             wrk.close();
  488.  
  489.             if (osUserTimeZone != null) {
  490.                 // Lookup timezone registry key in table of known tz keys
  491.                 // to convert to ZoneInfo timezone id.
  492.                 const regKeyToZoneInfoBundle =
  493.                     sbSvc.createBundle("chrome://calendar/content/"+
  494.                                        fileOSName+"ToZoneInfoTZId.properties");
  495.                 zoneInfoIdFromOSUserTimeZone =
  496.                     regKeyToZoneInfoBundle.GetStringFromName(osUserTimeZone);
  497.             }
  498.         } else {
  499.             // Else look for ZoneInfo timezone id in
  500.             // - TZ environment variable value
  501.             // - /etc/localtime symbolic link target path
  502.             // - /etc/TIMEZONE or /etc/timezone file content
  503.             // - /etc/sysconfig/clock file line content.
  504.             // The timezone is set per user via the TZ environment variable.
  505.             // TZ may contain a path that may start with a colon and ends with
  506.             // a ZoneInfo timezone identifier, such as ":America/New_York" or 
  507.             // ":/share/lib/zoneinfo/America/New_York".  The others are
  508.             // in the filesystem so they give one timezone for the system;
  509.             // the values are similar (but cannot have a leading colon).
  510.             // (Note: the OS ZoneInfo database may be a different version from
  511.             // the one we use, so still need to check that DST dates match.)
  512.             var continent = "Africa|America|Antarctica|Asia|Australia|Europe";
  513.             var ocean     = "Arctic|Atlantic|Indian|Pacific";
  514.             var tzRegex   = new RegExp(".*((?:"+continent+"|"+ocean+")"+
  515.                                        "(?:[/][-A-Z_a-z]+)+)");
  516.             const CC = Components.classes;
  517.             const CI = Components.interfaces;
  518.             var envSvc = (CC["@mozilla.org/process/environment;1"]
  519.                           .getService(Components.interfaces.nsIEnvironment));
  520.             function environmentVariableValue(varName) {
  521.                 var value = envSvc.get(varName);
  522.                 if (!value) return "";
  523.                 if (!value.match(tzRegex)) return "";
  524.                 return varName+"="+value;
  525.             }
  526.             function symbolicLinkTarget(filepath) {
  527.                 try {
  528.                     var file = (CC["@mozilla.org/file/local;1"]
  529.                                 .createInstance(CI.nsILocalFile));
  530.                     file.initWithPath(filepath);
  531.                     file.QueryInterface(CI.nsIFile);
  532.                     if (!file.exists()) return "";
  533.                     if (!file.isSymlink()) return "";
  534.                     if (!file.target.match(tzRegex)) return "";
  535.                     return filepath +" -> "+file.target;
  536.                 } catch (ex) {
  537.                     Components.utils.reportError(filepath+": "+ex);
  538.                     return "";
  539.                 }
  540.             }
  541.             function fileFirstZoneLineString(filepath) {
  542.                 // return first line of file that matches tzRegex (ZoneInfo id),
  543.                 // or "" if no file or no matching line.
  544.                 try {
  545.                     var file = (CC["@mozilla.org/file/local;1"]
  546.                                 .createInstance(CI.nsILocalFile));
  547.                     file.initWithPath(filepath);
  548.                     file.QueryInterface(CI.nsIFile);
  549.                     if (!file.exists()) return "";
  550.                     var fileInstream =
  551.                         (CC["@mozilla.org/network/file-input-stream;1"].
  552.                          createInstance(CI.nsIFileInputStream));
  553.                     const PR_RDONLY = 0x1;
  554.                     fileInstream.init(file, PR_RDONLY, 0, 0);
  555.                     fileInstream.QueryInterface(CI.nsILineInputStream);
  556.                     try { 
  557.                         var line = {}, hasMore = true, MAXLINES = 10;
  558.                         for (var i = 0; hasMore && i < MAXLINES; i++) { 
  559.                             hasMore = fileInstream.readLine(line);
  560.                             if (line.value && line.value.match(tzRegex)) { 
  561.                                 return filepath+": "+line.value;
  562.                             }
  563.                         }
  564.                         return ""; // not found
  565.                     } finally {
  566.                         fileInstream.close();
  567.                     }
  568.                 } catch (ex) {
  569.                     Components.utils.reportError(filepath+": "+ex);
  570.                     return "";
  571.                 }
  572.               
  573.             }
  574.             osUserTimeZone = (environmentVariableValue("TZ") ||
  575.                               symbolicLinkTarget("/etc/localtime") ||
  576.                               fileFirstZoneLineString("/etc/TIMEZONE") ||
  577.                               fileFirstZoneLineString("/etc/timezone") ||
  578.                               fileFirstZoneLineString("/etc/sysconfig/clock"));
  579.             var results = osUserTimeZone.match(tzRegex);
  580.             if (results) {
  581.                 zoneInfoIdFromOSUserTimeZone = results[1];
  582.             }
  583.         }
  584.  
  585.         // check how well OS tz matches tz defined in our version of zoneinfo db
  586.         if (zoneInfoIdFromOSUserTimeZone != null) { 
  587.             var tzId = tzSvc.tzidPrefix + zoneInfoIdFromOSUserTimeZone;
  588.             var score = checkTZ(tzId);
  589.             switch(score) {
  590.             case 0:
  591.                 // Did not match.
  592.                 // Maybe OS or Application is old, and the timezone changed.
  593.                 // Or maybe user turned off DST in Date/Time control panel.
  594.                 // Will look for a better matching tz, or fallback to floating.
  595.                 // (Match OS so alarms go off at time indicated by OS clock.)
  596.                 const consoleSvc =
  597.                     (Components.classes["@mozilla.org/consoleservice;1"]
  598.                      .getService(Components.interfaces.nsIConsoleService));
  599.                 var msg = (calProperties.formatStringFromName
  600.                            ("WarningOSTZNoMatch",
  601.                             [osUserTimeZone, zoneInfoIdFromOSUserTimeZone], 2));
  602.                 consoleSvc.logStringMessage(msg);
  603.                 break;
  604.             case 1: case 2:
  605.                 // inexact match: OS TZ and our ZoneInfo TZ matched imperfectly.
  606.                 // Will keep looking, will use tzId unless another is better.
  607.                 // (maybe OS TZ has changed to match a nearby TZ, so maybe
  608.                 // another ZoneInfo TZ matches it better).
  609.                 probableTZId = tzId;
  610.                 probableTZScore = score;
  611.                 probableTZSource = (calProperties.formatStringFromName
  612.                                     ("TZFromOS", [osUserTimeZone], 1));
  613.                 break;
  614.             case 3:
  615.                 // exact match
  616.                 return tzId;
  617.             }
  618.         }
  619.     } catch (ex) {
  620.         // zoneInfo id given was not recognized by our ZoneInfo database
  621.         var errMsg = (calProperties.formatStringFromName
  622.                       ("SkippingOSTimezone",
  623.                        [zoneInfoIdFromOSUserTimeZone || osUserTimeZone], 1));
  624.         Components.utils.reportError(errMsg+" "+ex);
  625.     } 
  626.  
  627.     // Second, give priority to "likelyTimezone"s if provided by locale.
  628.     try {
  629.         // The likelyTimezone property is a comma-separated list of 
  630.         // ZoneInfo timezone ids.
  631.         const bundleTZString =
  632.             calProperties.GetStringFromName("likelyTimezone");
  633.         const bundleTZIds = bundleTZString.split(/\s*,\s*/);
  634.         for each (var bareTZId in bundleTZIds) { 
  635.             var tzId = bareTZId; 
  636.             if (tzId.indexOf("/mozilla.org/") == -1) {
  637.                 // Convert a ZoneInfo timezone to a mozilla timezone-string
  638.                 tzId = tzSvc.tzidPrefix + tzId;
  639.             }
  640.             try { 
  641.                 var score = checkTZ(tzId);
  642.  
  643.                 switch (score) {
  644.                 case 0:
  645.                     break;
  646.                 case 1: case 2:
  647.                     if (score > probableTZScore) { 
  648.                         probableTZId = tzId;
  649.                         probableTZScore = score;
  650.                         probableTZSource = (calProperties.GetStringFromName
  651.                                             ("TZFromLocale"));
  652.                     }
  653.                     break;
  654.                 case 3:
  655.                     return tzId;
  656.                 }
  657.             } catch (ex) {
  658.                 var errMsg = (calProperties.formatStringFromName
  659.                               ("SkippingLocaleTimezone", [bareTZId], 1));
  660.                 Components.utils.reportError(errMsg+" "+ex);
  661.             }
  662.         }
  663.     } catch (ex) { // Oh well, this didn't work, next option...
  664.         Components.utils.reportError(ex);
  665.     }
  666.         
  667.     // Third, try all known timezones.
  668.     const tzIDs = tzSvc.timezoneIds;
  669.     while (tzIDs.hasMore()) {
  670.         var tzId = tzIDs.getNext();
  671.         try {
  672.             var score = checkTZ(tzId);
  673.             switch(score) { 
  674.             case 0: break;
  675.             case 1: case 2:
  676.                 if (score > probableTZScore) {
  677.                     probableTZId = tzId;
  678.                     probableTZScore = score;
  679.                     probableTZSource = (calProperties.GetStringFromName
  680.                                         ("TZFromKnownTimezones"));
  681.                 }
  682.                 break;
  683.             case 3:
  684.                 return tzId;
  685.             }
  686.         } catch (ex) { // bug if ics service doesn't recognize own tzid!
  687.             var msg = ("ics-service doesn't recognize own tzid: "+tzId+"\n"+
  688.                        ex);
  689.             Components.utils.reportError(msg);
  690.         }
  691.     }
  692.  
  693.     // If reach here, there were no score=3 matches, so Warn in console.
  694.     try { 
  695.         const jsConsole = Components.classes["@mozilla.org/consoleservice;1"]
  696.                            .getService(Components.interfaces.nsIConsoleService);
  697.         switch(probableTZScore) {
  698.         case 0:
  699.             jsConsole.logStringMessage(calProperties.GetStringFromName
  700.                                        ("warningUsingFloatingTZNoMatch"));
  701.             break;
  702.         case 1: case 2:
  703.             var tzId = probableTZId;
  704.             var tz = tzSvc.getTimezone(tzId);
  705.             var subComp = tz.component;
  706.             var standard = findCurrentTimePeriod(tz, subComp, "STANDARD");
  707.             var standardTZOffset = getIcalString(standard, "TZOFFSETTO");
  708.             var daylight = findCurrentTimePeriod(tz, subComp, "DAYLIGHT");
  709.             var daylightTZOffset = getIcalString(daylight, "TZOFFSETTO");
  710.             var warningDetail;
  711.             if (probableTZScore == 1) {
  712.                 // score 1 means has daylight time,
  713.                 // but transitions start on different weekday from os timezone.
  714.                 function weekday(icsDate) {
  715.                     var calDate = createDateTime();
  716.                     calDate.timezone = tz;
  717.                     calDate.icalString = icsDate;
  718.                     return calDate.jsDate.toLocaleFormat("%a");
  719.                 }
  720.                 var standardStart = getIcalString(standard, "DTSTART");
  721.                 var standardStartWeekday = weekday(standardStart);
  722.                 var standardRule  = getIcalString(standard, "RRULE");
  723.                 var standardText = 
  724.                     ("  Standard: "+standardStart+" "+standardStartWeekday+"\n"+
  725.                      "            "+standardRule+"\n");
  726.                 var daylightStart = getIcalString(daylight, "DTSTART");
  727.                 var daylightStartWeekday = weekday(daylightStart);
  728.                 var daylightRule  = getIcalString(daylight, "RRULE");
  729.                 var daylightText =
  730.                     ("  Daylight: "+daylightStart+" "+daylightStartWeekday+"\n"+
  731.                      "            "+daylightRule+"\n");
  732.                 warningDetail =
  733.                     ((standardStart < daylightStart
  734.                       ? standardText + daylightText
  735.                       : daylightText + standardText)+
  736.                      (calProperties.GetStringFromName
  737.                       ("TZAlmostMatchesOSDifferAtMostAWeek")));
  738.             } else {
  739.                 warningDetail =
  740.                     (calProperties.GetStringFromName("TZSeemsToMatchOS"));
  741.             }
  742.             var offsetString = (standardTZOffset+
  743.                                  (!daylightTZOffset? "": "/"+daylightTZOffset));
  744.             var warningMsg = (calProperties.formatStringFromName
  745.                               ("WarningUsingGuessedTZ",
  746.                                [tzId, offsetString, warningDetail,
  747.                                 probableTZSource], 4));
  748.             jsConsole.logStringMessage(warningMsg);
  749.             break;
  750.         }
  751.     } catch (ex) { // don't abort if error occurs warning user
  752.         Components.utils.reportError(ex);
  753.     }
  754.  
  755.     // return the guessed timezone
  756.     return probableTZId;
  757. }
  758.  
  759. /**
  760.  * Shared dialog functions
  761.  * Gets the calendar directory, defaults to <profile-dir>/calendar
  762.  */
  763. function getCalendarDirectory() {
  764.     if (getCalendarDirectory.mDir === undefined) {
  765.         var dirSvc = Components.classes["@mozilla.org/file/directory_service;1"]
  766.                                .getService(Components.interfaces.nsIProperties);
  767.         var dir = dirSvc.get("ProfD", Components.interfaces.nsILocalFile);
  768.         dir.append("calendar-data");
  769.         if (!dir.exists()) {
  770.             try {
  771.                 dir.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0700);
  772.             } catch (exc) {
  773.                 ASSERT(false, exc);
  774.                 throw exc;
  775.             }
  776.         }
  777.         getCalendarDirectory.mDir = dir;
  778.     }
  779.     return getCalendarDirectory.mDir.clone();
  780. }
  781.  
  782. /**
  783.  * Check if the specified calendar is writable. This is the case when it is not
  784.  * marked readOnly, we are not offline, or we are offline and the calendar is
  785.  * local.
  786.  *
  787.  * @param aCalendar     The calendar to check
  788.  * @return              True if the calendar is writable
  789.  */
  790. function isCalendarWritable(aCalendar) {
  791.     return (!aCalendar.readOnly &&
  792.            (!getIOService().offline ||
  793.             aCalendar.getProperty("requiresNetwork") === false));
  794. }
  795.  
  796. /**
  797.  * Opens the Create Calendar wizard
  798.  *
  799.  * @param aCallback  a function to be performed after calendar creation
  800.  */
  801. function openCalendarWizard(aCallback) {
  802.     openDialog("chrome://calendar/content/calendarCreation.xul", "caEditServer",
  803.                "chrome,titlebar,modal", aCallback);
  804. }
  805.  
  806. /**
  807.  * Opens the calendar properties window for aCalendar
  808.  *
  809.  * @param aCalendar  the calendar whose properties should be displayed
  810.  * @param aCallback  function that should be run when the dialog is accepted
  811.  */
  812. function openCalendarProperties(aCalendar, aCallback) {
  813.     openDialog("chrome://calendar/content/calendarProperties.xul",
  814.                "caEditServer", "chrome,titlebar,modal",
  815.                {calendar: aCalendar, onOk: aCallback});
  816. }
  817.  
  818. /**
  819.  * Opens the print dialog
  820.  */
  821. function calPrint() {
  822.     openDialog("chrome://calendar/content/printDialog.xul", "Print",
  823.                "centerscreen,chrome,resizable");
  824. }
  825.  
  826. /**
  827.  * Other functions
  828.  */
  829.  
  830. /**
  831.  * Takes a string and returns an nsIURI
  832.  *
  833.  * @param aUriString  the string of the address to for the spec of the nsIURI
  834.  *
  835.  * @returns  an nsIURI whose spec is aUriString
  836.  */
  837. function makeURL(aUriString) {
  838.     var ioSvc = Components.classes["@mozilla.org/network/io-service;1"].
  839.                 getService(Components.interfaces.nsIIOService);
  840.     return ioSvc.newURI(aUriString, null, null);
  841. }
  842.  
  843. /**
  844.  * Returns a calIDateTime that corresponds to the current time in the user's
  845.  * default timezone.
  846.  */
  847. function now() {
  848.     var d = createDateTime();
  849.     d.jsDate = new Date();
  850.     return d.getInTimezone(calendarDefaultTimezone());
  851. }
  852.  
  853. /**
  854.  * Returns a calIDateTime corresponding to a javascript Date.
  855.  *
  856.  * @param aDate     a javascript date
  857.  * @param aTimezone (optional) a timezone that should be enforced
  858.  * @returns         a calIDateTime
  859.  *
  860.  * @warning  Use of this function is strongly discouraged.  calIDateTime should
  861.  *           be used directly whenever possible.
  862.  *           If you pass a timezone, then the passed jsDate's timezone will be ignored,
  863.  *           but only its local time portions are be taken.
  864.  */
  865. function jsDateToDateTime(aDate, aTimezone) {
  866.     var newDate = createDateTime();
  867.     if (aTimezone) {
  868.         newDate.resetTo(aDate.getFullYear(),
  869.                         aDate.getMonth(),
  870.                         aDate.getDate(),
  871.                         aDate.getHours(),
  872.                         aDate.getMinutes(),
  873.                         aDate.getSeconds(),
  874.                         aTimezone);
  875.     } else {
  876.         newDate.jsDate = aDate;
  877.     }
  878.     return newDate;
  879. }
  880.  
  881. /**
  882.  * Selects an item with id aItemId in the radio group with id aRadioGroupId
  883.  *
  884.  * @param aRadioGroupId  the id of the radio group which contains the item
  885.  * @param aItemId        the item to be selected
  886.  */
  887. function calRadioGroupSelectItem(aRadioGroupId, aItemId) {
  888.     var radioGroup = document.getElementById(aRadioGroupId);
  889.     var items = radioGroup.getElementsByTagName("radio");
  890.     var index;
  891.     for (var i in items) {
  892.         if (items[i].getAttribute("id") == aItemId) {
  893.             index = i;
  894.             break;
  895.         }
  896.     }
  897.     ASSERT(index && index != 0, "Can't find radioGroup item to select.", true);
  898.     radioGroup.selectedIndex = index;
  899. }
  900.  
  901.  
  902. /** checks if an item is supported by a Calendar
  903. * @param aCalendar the calendar
  904. * @param aItem the item either a task or an event
  905. * @return true or false
  906. */
  907. function isItemSupported(aItem, aCalendar) {
  908.     if (isToDo(aItem)) {
  909.         return (aCalendar.getProperty("capabilities.tasks.supported") !== false);
  910.     } else if (isEvent(aItem)) {
  911.         return (aCalendar.getProperty("capabilities.events.supported") !== false);
  912.     }
  913.     return false;
  914. }
  915.  
  916. /**
  917.  * Determines whether or not the aObject is a calIEvent
  918.  *
  919.  * @param aObject  the object to test
  920.  * @returns        true if the object is a calIEvent, false otherwise
  921.  */
  922. function isEvent(aObject) {
  923.     return aObject instanceof Components.interfaces.calIEvent;
  924. }
  925.  
  926. /**
  927.  * Determines whether or not the aObject is a calITodo
  928.  *
  929.  * @param aObject  the object to test
  930.  * @returns        true if the object is a calITodo, false otherwise
  931.  */
  932. function isToDo(aObject) {
  933.     return aObject instanceof Components.interfaces.calITodo;
  934. }
  935.  
  936. /**
  937.  * Normal get*Pref calls will throw if the pref is undefined.  This function
  938.  * will get a bool, int, or string pref.  If the pref is undefined, it will
  939.  * return aDefault.
  940.  *
  941.  * @param aPrefName   the (full) name of preference to get
  942.  * @param aDefault    (optional) the value to return if the pref is undefined
  943.  */
  944. function getPrefSafe(aPrefName, aDefault) {
  945.     const nsIPrefBranch = Components.interfaces.nsIPrefBranch;
  946.     const prefB = Components.classes["@mozilla.org/preferences-service;1"]
  947.                             .getService(nsIPrefBranch);
  948.     // Since bug 193332 does not fix the current branch, calling get*Pref will
  949.     // throw NS_ERROR_UNEXPECTED if clearUserPref() was called and there is no
  950.     // default value. To work around that, catch the exception.
  951.     try {
  952.         switch (prefB.getPrefType(aPrefName)) {
  953.             case nsIPrefBranch.PREF_BOOL:
  954.                 return prefB.getBoolPref(aPrefName);
  955.             case nsIPrefBranch.PREF_INT:
  956.                 return prefB.getIntPref(aPrefName);
  957.             case nsIPrefBranch.PREF_STRING:
  958.                 return prefB.getCharPref(aPrefName);
  959.             default: // includes nsIPrefBranch.PREF_INVALID
  960.                 return aDefault;
  961.         }
  962.     } catch (e) {
  963.         return aDefault;
  964.     }
  965. }
  966.  
  967. /**
  968.  * Wrapper for setting prefs of various types
  969.  *
  970.  * @param aPrefName   the (full) name of preference to set
  971.  * @param aPrefType   the type of preference to set.  Valid valuse are:
  972.                         BOOL, INT, and CHAR
  973.  * @param aPrefValue  the value to set the pref to
  974.  */
  975. function setPref(aPrefName, aPrefType, aPrefValue) {
  976.     const nsIPrefBranch = Components.interfaces.nsIPrefBranch;
  977.     const prefB = Components.classes["@mozilla.org/preferences-service;1"]
  978.                             .getService(nsIPrefBranch);
  979.     switch (aPrefType) {
  980.         case "BOOL":
  981.             prefB.setBoolPref(aPrefName, aPrefValue);
  982.             break;
  983.         case "INT":
  984.             prefB.setIntPref(aPrefName, aPrefValue);
  985.             break;
  986.         case "CHAR":
  987.             prefB.setCharPref(aPrefName, aPrefValue);
  988.             break;
  989.     }
  990. }
  991.  
  992. /**
  993.  * Helper function to set a localized (complex) pref from a given string
  994.  *
  995.  * @param aPrefName   the (full) name of preference to set
  996.  * @param aString     the string to which the preference value should be set
  997.  */
  998. function setLocalizedPref(aPrefName, aString) {
  999.     const prefB = Components.classes["@mozilla.org/preferences-service;1"].
  1000.                   getService(Components.interfaces.nsIPrefBranch);
  1001.     var str = Components.classes["@mozilla.org/supports-string;1"].
  1002.               createInstance(Components.interfaces.nsISupportsString);
  1003.     str.data = aString;
  1004.     prefB.setComplexValue(aPrefName, Components.interfaces.nsISupportsString, str);
  1005. }
  1006.  
  1007. /**
  1008.  * Like getPrefSafe, except for complex prefs (those used for localized data).
  1009.  *
  1010.  * @param aPrefName   the (full) name of preference to get
  1011.  * @param aDefault    (optional) the value to return if the pref is undefined
  1012.  */
  1013. function getLocalizedPref(aPrefName, aDefault) {
  1014.     const pb2 = Components.classes["@mozilla.org/preferences-service;1"].
  1015.                 getService(Components.interfaces.nsIPrefBranch2);
  1016.     var result;
  1017.     try {
  1018.         result = pb2.getComplexValue(aPrefName, Components.interfaces.nsISupportsString).data;
  1019.     } catch(ex) {
  1020.         return aDefault;
  1021.     }
  1022.     return result;
  1023. }
  1024.  
  1025. /**
  1026.  * Get array of category names from preferences or locale default,
  1027.  * unescaping any commas in each category name.
  1028.  * @return array of category names
  1029.  */
  1030. function getPrefCategoriesArray() {
  1031.     var categories = getLocalizedPref("calendar.categories.names", null);
  1032.     // If no categories are configured load a default set from properties file
  1033.     if (!categories || categories == "") {
  1034.         categories = calGetString("categories", "categories");
  1035.         setLocalizedPref("calendar.categories.names", categories);
  1036.     }
  1037.     return categoriesStringToArray(categories);
  1038. }
  1039.  
  1040. /**
  1041.  * Convert categories string to list of category names.
  1042.  *
  1043.  * Stored categories may include escaped commas within a name.
  1044.  * Split categories string at commas, but not at escaped commas (\,).
  1045.  * Afterward, replace escaped commas (\,) with commas (,) in each name.
  1046.  * @param aCategoriesPrefValue string from "calendar.categories.names" pref,
  1047.  * which may contain escaped commas (\,) in names.
  1048.  * @return list of category names
  1049.  */
  1050. function categoriesStringToArray(aCategories) {
  1051.     // \u001A is the unicode "SUBSTITUTE" character
  1052.     function revertCommas(name) { return name.replace(/\u001A/g, ","); }
  1053.     return aCategories.replace(/\\,/g, "\u001A").split(",").map(revertCommas);
  1054. }
  1055.  
  1056. /**
  1057.  * Set categories preference, escaping any commas in category names.
  1058.  * @param aCategoriesArray array of category names,
  1059.  * may contain unescaped commas which will be escaped in combined pref.
  1060.  */
  1061. function setPrefCategoriesFromArray(aCategoriesArray) {
  1062.     setLocalizedPref("calendar.categories.names",
  1063.                      categoriesArrayToString(aCategoriesList));
  1064. }
  1065.  
  1066. /**
  1067.  * Convert array of category names to string.
  1068.  *
  1069.  * Category names may contain commas (,).  Escape commas (\,) in each,
  1070.  * then join them in comma separated string for storage.
  1071.  * @param aSortedCategoriesArray sorted array of category names,
  1072.  * may contain unescaped commas, which will be escaped in combined string.
  1073.  */
  1074. function categoriesArrayToString(aSortedCategoriesArray) {
  1075.     function escapeComma(category) { return category.replace(/,/g,"\\,"); }
  1076.     return aSortedCategoriesArray.map(escapeComma).join(",");
  1077. }
  1078.  
  1079. /**
  1080.  * Sort an array of strings according to the current locale.
  1081.  * Modifies aStringArray, returning it sorted.
  1082.  */
  1083. function sortArrayByLocaleCollator(aStringArray) {
  1084.     // get a current locale string collator for compareEvents
  1085.     var localeService =
  1086.         Components
  1087.         .classes["@mozilla.org/intl/nslocaleservice;1"]
  1088.         .getService(Components.interfaces.nsILocaleService);
  1089.     var localeCollator =
  1090.         Components
  1091.         .classes["@mozilla.org/intl/collation-factory;1"]
  1092.         .getService(Components.interfaces.nsICollationFactory)
  1093.         .CreateCollation(localeService.getApplicationLocale());
  1094.     function compare(a, b) { return localeCollator.compareString(0, a, b); }
  1095.     aStringArray.sort(compare);
  1096.     return aStringArray;
  1097. }
  1098.  
  1099. /**
  1100.  * Gets the value of a string in a .properties file
  1101.  *
  1102.  * @param aBundleName  the name of the properties file.  It is assumed that the
  1103.  *                     file lives in chrome://calendar/locale/
  1104.  * @param aStringName  the name of the string within the properties file
  1105.  * @param aParams      optional array of parameters to format the string
  1106.  */
  1107. function calGetString(aBundleName, aStringName, aParams) {
  1108.     try {
  1109.         var sbs = Components.classes["@mozilla.org/intl/stringbundle;1"]
  1110.                             .getService(Components.interfaces.nsIStringBundleService);
  1111.         var props = sbs.createBundle("chrome://calendar/locale/"+aBundleName+".properties");
  1112.  
  1113.         if (aParams && aParams.length) {
  1114.             return props.formatStringFromName(aStringName, aParams, aParams.length);
  1115.         } else {
  1116.             return props.GetStringFromName(aStringName);
  1117.         }
  1118.     } catch (ex) {
  1119.         var s = "Failed to read '" + aStringName + "' from " +
  1120.                 "'chrome://calendar/locale/" + aBundleName + ".properties'.";
  1121.         Components.utils.reportError(s + " Error: " + ex);
  1122.         return s;
  1123.     }
  1124. }
  1125.  
  1126. /** Returns a best effort at making a UUID.  If we have the UUIDGenerator
  1127.  * service available, we'll use that.  If we're somewhere where it doesn't
  1128.  * exist, like Lightning in TB 1.5, we'll just use the current time.
  1129.  */
  1130. function getUUID() {
  1131.     if ("@mozilla.org/uuid-generator;1" in Components.classes) {
  1132.         var uuidGen = Components.classes["@mozilla.org/uuid-generator;1"].
  1133.                       getService(Components.interfaces.nsIUUIDGenerator);
  1134.         // generate uuids without braces to avoid problems with 
  1135.         // CalDAV servers that don't support filenames with {}
  1136.         return uuidGen.generateUUID().toString().replace(/[{}]/g, '');
  1137.     }
  1138.     // No uuid service (we're on the 1.8.0 branch)
  1139.     return "uuid" + (new Date()).getTime();
  1140. }
  1141.  
  1142. /** Due to a bug in js-wrapping, normal == comparison can fail when we
  1143.  * have 2 objects.  Use these functions to force them both to get wrapped
  1144.  * the same way, allowing for normal comparison.
  1145.  */
  1146.  
  1147. /**
  1148.  * calIItemBase comparer
  1149.  */
  1150. function compareItems(aItem, aOtherItem) {
  1151.     var sip1 = Components.classes["@mozilla.org/supports-interface-pointer;1"].
  1152.                createInstance(Components.interfaces.nsISupportsInterfacePointer);
  1153.     sip1.data = aItem;
  1154.     sip1.dataIID = Components.interfaces.calIItemBase;
  1155.  
  1156.     var sip2 = Components.classes["@mozilla.org/supports-interface-pointer;1"].
  1157.                createInstance(Components.interfaces.nsISupportsInterfacePointer);
  1158.     sip2.data = aOtherItem;
  1159.     sip2.dataIID = Components.interfaces.calIItemBase;
  1160.     return sip1.data == sip2.data;
  1161. }
  1162.  
  1163. /**
  1164.  * Generic object comparer
  1165.  * Use to compare two objects which are not of type calIItemBase, in order
  1166.  * to avoid the js-wrapping issues mentioned above.
  1167.  *
  1168.  * @param aObject        first object to be compared
  1169.  * @param aOtherObject   second object to be compared
  1170.  * @param aIID           IID to use in comparison, undefined/null defaults to nsISupports
  1171.  */
  1172. function compareObjects(aObject, aOtherObject, aIID) {
  1173.     // xxx todo: seems to work fine e.g. for WCAP, but I still mistrust this trickery...
  1174.     //           Anybody knows an official API that could be used for this purpose?
  1175.     //           For what reason do clients need to pass aIID since
  1176.     //           every XPCOM object has to implement nsISupports?
  1177.     //           XPCOM (like COM, like UNO, ...) defines that QueryInterface *only* needs to return
  1178.     //           the very same pointer for nsISupports during its lifetime.
  1179.     if (!aIID) {
  1180.         aIID = Components.interfaces.nsISupports;
  1181.     }
  1182.     var sip1 = Components.classes["@mozilla.org/supports-interface-pointer;1"].
  1183.                createInstance(Components.interfaces.nsISupportsInterfacePointer);
  1184.     sip1.data = aObject;
  1185.     sip1.dataIID = aIID;
  1186.  
  1187.     var sip2 = Components.classes["@mozilla.org/supports-interface-pointer;1"].
  1188.                createInstance(Components.interfaces.nsISupportsInterfacePointer);
  1189.     sip2.data = aOtherObject;
  1190.     sip2.dataIID = aIID;
  1191.     return sip1.data == sip2.data;
  1192. }
  1193.  
  1194. /**
  1195.  * Compare two arrays using the passed function.
  1196.  */
  1197. function compareArrays(aOne, aTwo, compareFunc) {
  1198.     if (!aOne && !aTwo)
  1199.         return true;
  1200.     if (!aOne || !aTwo)
  1201.         return false;
  1202.     var len = aOne.length;
  1203.     if (len != aTwo.length)
  1204.         return false;
  1205.     for (var i = 0; i < len; ++i) {
  1206.         if (!compareFunc(aOne[i], aTwo[i]))
  1207.             return false;
  1208.     }
  1209.     return true;
  1210. }
  1211.  
  1212. /**
  1213.  * Ensures the passed IID is in the list, else throws Components.results.NS_ERROR_NO_INTERFACE.
  1214.  */
  1215. function ensureIID(aList, aIID) {
  1216.     for each (var iid in aList) {
  1217.         if (aIID.equals(iid))
  1218.             return;
  1219.     }
  1220.     throw Components.results.NS_ERROR_NO_INTERFACE
  1221. }
  1222.  
  1223. /**
  1224.  * Takes care of all QueryInterface business, including calling the QI of any
  1225.  * existing parent prototypes.
  1226.  *
  1227.  * @param aSelf         The object the QueryInterface is being made to
  1228.  * @param aProto        Caller's prototype object
  1229.  * @param aIID          The IID to check for
  1230.  * @param aList         (Optional if aClassInfo is specified) An array of
  1231.  *                        interfaces from Components.interfaces
  1232.  * @param aClassInfo    (Optional) an Object containing the class info for this
  1233.  *                        prototype.
  1234.  */
  1235. function doQueryInterface(aSelf, aProto, aIID, aList, aClassInfo) {
  1236.     if (aClassInfo && aIID.equals(Components.interfaces.nsIClassInfo)) {
  1237.         return aClassInfo;
  1238.     }
  1239.  
  1240.     var list = aList;
  1241.     if (!list && aClassInfo) {
  1242.         list = aClassInfo.getInterfaces({});
  1243.     }
  1244.  
  1245.     var list = aList;
  1246.     if (!list && aClassInfo) {
  1247.         list = aClassInfo.getInterfaces({});
  1248.     }
  1249.  
  1250.     for each (var iid in list) {
  1251.         if (aIID.equals(iid))
  1252.             return aSelf;
  1253.     }
  1254.  
  1255.     var base = aProto.__proto__;
  1256.  
  1257.     if (base && base.QueryInterface) {
  1258.         // Try to QI the base prototype
  1259.         return base.QueryInterface.call(aSelf, aIID);
  1260.     }
  1261.     throw Components.results.NS_ERROR_NO_INTERFACE;
  1262. }
  1263.  
  1264. /**
  1265.  * Many computations want to work only with date-times, not with dates.  This
  1266.  * method will return a proper datetime (set to midnight) for a date object.  If
  1267.  * the object is already a datetime, it will simply be returned.
  1268.  *
  1269.  * @param aDate  the date or datetime to check
  1270.  */
  1271. function ensureDateTime(aDate) {
  1272.     if (!aDate || !aDate.isDate) {
  1273.         return aDate;
  1274.     }
  1275.     var newDate = aDate.clone();
  1276.     newDate.isDate = false;
  1277.     return newDate;
  1278. }
  1279.  
  1280. /****
  1281.  **** debug code
  1282.  ****/
  1283.  
  1284. /**
  1285.  * Logs a string or an object to both stderr and the js-console only in the case 
  1286.  * where the calendar.debug.log pref is set to true.
  1287.  *
  1288.  * @param aArg  either a string to log or an object whose entire set of 
  1289.  *              properties should be logged.
  1290.  */
  1291. function LOG(aArg) {
  1292.     var prefB = Components.classes["@mozilla.org/preferences-service;1"].
  1293.                 getService(Components.interfaces.nsIPrefBranch);
  1294.     var shouldLog = false;
  1295.     try {
  1296.         shouldLog = prefB.getBoolPref("calendar.debug.log");
  1297.     } catch(ex) {}
  1298.  
  1299.     if (!shouldLog) {
  1300.         return;
  1301.     }
  1302.     ASSERT(aArg, "Bad log argument.", false);
  1303.     var string;
  1304.     // We should just dump() both String objects, and string primitives.
  1305.     if (!(aArg instanceof String) && !(typeof(aArg) == "string")) {
  1306.         var string = "Logging object...\n";
  1307.         for (var prop in aArg) {
  1308.             string += prop + ': ' + aArg[prop] + '\n';
  1309.         }
  1310.         string += "End object\n";
  1311.     } else {
  1312.         string = aArg;
  1313.     }
  1314.  
  1315.     dump(string + '\n');
  1316.     var consoleSvc = Components.classes["@mozilla.org/consoleservice;1"].
  1317.                      getService(Components.interfaces.nsIConsoleService);
  1318.     consoleSvc.logStringMessage(string);
  1319. }
  1320.  
  1321. /**
  1322.  * Dumps a warning to both console and js console.
  1323.  *
  1324.  * @param aMessage warning message
  1325.  */
  1326. function WARN(aMessage) {
  1327.     dump("Warning: " + aMessage + '\n');
  1328.     var scriptError = Components.classes["@mozilla.org/scripterror;1"]
  1329.                                 .createInstance(Components.interfaces.nsIScriptError);
  1330.     scriptError.init(aMessage, null, null, 0, 0,
  1331.                      Components.interfaces.nsIScriptError.warningFlag,
  1332.                      "component javascript");
  1333.     var consoleSvc = Components.classes["@mozilla.org/consoleservice;1"]
  1334.                                .getService(Components.interfaces.nsIConsoleService);
  1335.     consoleSvc.logMessage(scriptError);
  1336. }
  1337.  
  1338. /**
  1339.  * Returns a string describing the current js-stack with filename and line
  1340.  * numbers.
  1341.  *
  1342.  * @param aDepth (optional) The number of frames to include. Defaults to 5.
  1343.  */
  1344. function STACK(aDepth) {
  1345.     var depth = aDepth || 5;
  1346.     var stack = "";
  1347.     var frame = Components.stack.caller;
  1348.     for (var i = 1; i <= depth && frame; i++) {
  1349.         stack += i + ": [" + frame.filename + ":" +
  1350.                  frame.lineNumber + "] " + frame.name + "\n";
  1351.         frame = frame.caller;
  1352.     }
  1353.     return stack;
  1354. }
  1355.  
  1356. /**
  1357.  * Logs a message and the current js-stack, if aCondition fails
  1358.  *
  1359.  * @param aCondition  the condition to test for
  1360.  * @param aMessage    the message to report in the case the assert fails
  1361.  * @param aCritical   if true, throw an error to stop current code execution
  1362.  *                    if false, code flow will continue
  1363.  */
  1364. function ASSERT(aCondition, aMessage, aCritical) {
  1365.     if (aCondition) {
  1366.         return;
  1367.     }
  1368.  
  1369.     var string = "Assert failed: " + aMessage + '\n' + STACK();
  1370.     if (aCritical) {
  1371.         throw new Error(string);
  1372.     } else {
  1373.         Components.utils.reportError(string);
  1374.     }
  1375. }
  1376.  
  1377. /**
  1378.  * Uses the prompt service to display an error message.
  1379.  *
  1380.  * @param aMsg The message to be shown
  1381.  */
  1382. function showError(aMsg) {
  1383.     var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
  1384.                                   .getService(Components.interfaces.nsIPromptService);
  1385.  
  1386.     promptService.alert(window,
  1387.                         calGetString("calendar", "errorTitle"),
  1388.                         aMsg);
  1389. }
  1390.  
  1391. /**
  1392.  * Auth prompt implementation - Uses password manager if at all possible.
  1393.  */
  1394. function calAuthPrompt() {
  1395.     // use the window watcher service to get a nsIAuthPrompt impl
  1396.     this.mPrompter = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
  1397.                                .getService(Components.interfaces.nsIWindowWatcher)
  1398.                                .getNewAuthPrompter(null);
  1399.     this.mTriedStoredPassword = false;
  1400. }
  1401.  
  1402. calAuthPrompt.prototype = {
  1403.     prompt: function capP(aDialogTitle, aText, aPasswordRealm, aSavePassword,
  1404.                           aDefaultText, aResult) {
  1405.         return this.mPrompter.prompt(aDialogTitle, aText, aPasswordRealm,
  1406.                                      aSavePassword, aDefaultText, aResult);
  1407.     },
  1408.  
  1409.     getPasswordInfo: function capGPI(aPasswordRealm) {
  1410.         var username;
  1411.         var password;
  1412.         var found = false;
  1413.  
  1414.         if ("@mozilla.org/passwordmanager;1" in Components.classes) {
  1415.             var passwordManager = Components.classes["@mozilla.org/passwordmanager;1"]
  1416.                                             .getService(Components.interfaces.nsIPasswordManager);
  1417.             var passwordRealm = aPasswordRealm.passwordRealm || aPasswordRealm;
  1418.             var pwenum = passwordManager.enumerator;
  1419.             // step through each password in the password manager until we find the one we want:
  1420.             while (pwenum.hasMoreElements()) {
  1421.                 try {
  1422.                     var pass = pwenum.getNext().QueryInterface(Components.interfaces.nsIPassword);
  1423.                     if (pass.host == passwordRealm) {
  1424.                          // found it!
  1425.                          username = pass.user;
  1426.                          password = pass.password;
  1427.                          found = true;
  1428.                     }
  1429.                 } catch (ex) {
  1430.                          found = true;
  1431.                          break;
  1432.                     // don't do anything here, ignore the password that could not
  1433.                     // be read
  1434.                 }
  1435.             }
  1436.         } else {
  1437.             var loginManager = Components.classes["@mozilla.org/login-manager;1"]
  1438.                                          .getService(Components.interfaces
  1439.                                          .nsILoginManager);
  1440.             var logins = loginManager.findLogins({}, aPasswordRealm.prePath, null,
  1441.                                                  aPasswordRealm.realm);
  1442.             if (logins.length) {
  1443.                 username = logins[0].username;
  1444.                 password = logins[0].password;
  1445.                 found = true;
  1446.             }
  1447.         }
  1448.         return {found: found, username: username, password: password};
  1449.     },
  1450.  
  1451.     promptUsernameAndPassword: function capPUAP(aDialogTitle, aText,
  1452.                                                 aPasswordRealm,aSavePassword,
  1453.                                                 aUser, aPwd) {
  1454.         var pw;
  1455.         if (!this.mTriedStoredPassword) {
  1456.             pw = this.getPasswordInfo(aPasswordRealm);
  1457.         }
  1458.  
  1459.         if (pw && pw.found) {
  1460.             this.mTriedStoredPassword = true;
  1461.             aUser.value = pw.username;
  1462.             aPwd.value = pw.password;
  1463.             return true;
  1464.         } else {
  1465.             return this.mPrompter.promptUsernameAndPassword(aDialogTitle, aText,
  1466.                                                             aPasswordRealm,
  1467.                                                             aSavePassword,
  1468.                                                             aUser, aPwd);
  1469.         }
  1470.     },
  1471.  
  1472.     // promptAuth is needed/used on trunk only
  1473.     promptAuth: function capPA(aChannel, aLevel, aAuthInfo) {
  1474.         var hostRealm = {};
  1475.         hostRealm.prePath = aChannel.URI.prePath;
  1476.         hostRealm.realm = aAuthInfo.realm;
  1477.         hostRealm.passwordRealm = aChannel.URI.host + ":" + aChannel.URI.port +
  1478.                                           " (" + aAuthInfo.realm + ")";
  1479.  
  1480.         var pw;
  1481.         if (!this.mTriedStoredPassword) {
  1482.             pw = this.getPasswordInfo(hostRealm);
  1483.         }
  1484.         if (pw && pw.found) {
  1485.             this.mTriedStoredPassword = true;
  1486.             aAuthInfo.username = pw.username;
  1487.             aAuthInfo.password = pw.password;
  1488.             return true;
  1489.         } else {
  1490.             var prompter2 =
  1491.                 Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
  1492.                           .getService(Components.interfaces.nsIPromptFactory)
  1493.                           .getPrompt(null, Components.interfaces.nsIAuthPrompt2);
  1494.             return prompter2.promptAuth(aChannel, aLevel, aAuthInfo);
  1495.         }
  1496.     },
  1497.  
  1498.     promptPassword: function capPP(aDialogTitle, aText, aPasswordRealm,
  1499.                              aSavePassword, aPwd) {
  1500.         var found = false;
  1501.         var pw;
  1502.         if (!this.mTriedStoredPassword) {
  1503.             pw = this.getPasswordInfo(aPasswordRealm);
  1504.         }
  1505.  
  1506.         if (pw && pw.found) {
  1507.             this.mTriedStoredPassword = true;
  1508.             aPwd.value = pw.password;
  1509.             return true;
  1510.         } else {
  1511.             return this.mPrompter.promptPassword(aDialogTitle, aText,
  1512.                                                  aPasswordRealm, aSavePassword,
  1513.                                                  aPwd);
  1514.         }
  1515.     }
  1516. }
  1517.  
  1518. /**
  1519.  * Pick whichever of "black" or "white" will look better when used as a text
  1520.  * color against a background of bgColor. 
  1521.  *
  1522.  * @param bgColor   the background color as a "#RRGGBB" string
  1523.  */
  1524. function getContrastingTextColor(bgColor)
  1525. {
  1526.     var calcColor = bgColor.replace(/#/g, "");
  1527.     var red = parseInt(calcColor.substring(0, 2), 16);
  1528.     var green = parseInt(calcColor.substring(2, 4), 16);
  1529.     var blue = parseInt(calcColor.substring(4, 6), 16);
  1530.  
  1531.     // Calculate the brightness (Y) value using the YUV color system.
  1532.     var brightness = (0.299 * red) + (0.587 * green) + (0.114 * blue);
  1533.  
  1534.     // Consider all colors with less than 56% brightness as dark colors and
  1535.     // use white as the foreground color, otherwise use black.
  1536.     if (brightness < 144) {
  1537.         return "white";
  1538.     }
  1539.  
  1540.     return "black";
  1541. }
  1542.  
  1543. /**
  1544.  * Returns the start date of an item, ie either an event's start date or a task's entry date.
  1545.  */
  1546. function calGetStartDate(aItem)
  1547. {
  1548.     return (isEvent(aItem) ? aItem.startDate : aItem.entryDate);
  1549. }
  1550.  
  1551. /**
  1552.  * Returns the end date of an item, ie either an event's end date or a task's due date.
  1553.  */
  1554. function calGetEndDate(aItem)
  1555. {
  1556.     return (isEvent(aItem) ? aItem.endDate : aItem.dueDate);
  1557. }
  1558.  
  1559. /**
  1560.  * Checks whether the passed item fits into the demanded range.
  1561.  *
  1562.  * @param item               the item
  1563.  * @param rangeStart         (inclusive) range start or null (open range)
  1564.  * @param rangeStart         (exclusive) range end or null (open range)
  1565.  * @param returnDtstartOrDue returns item's start (or due) date in case
  1566.  *                           the item is in the specified Range; null otherwise.
  1567.  */
  1568. function checkIfInRange(item, rangeStart, rangeEnd, returnDtstartOrDue)
  1569. {
  1570.     var startDate;
  1571.     var endDate;
  1572.     if (isEvent(item)) {
  1573.         startDate = item.startDate;
  1574.         if (!startDate) { // DTSTART mandatory
  1575.             // xxx todo: should we assert this case?
  1576.             return null;
  1577.         }
  1578.         endDate = (item.endDate || startDate);
  1579.     } else {
  1580.         var dueDate = item.dueDate;
  1581.         startDate = (item.entryDate || dueDate);
  1582.         if (!startDate) {
  1583.             if (returnDtstartOrDue) { // DTSTART or DUE mandatory
  1584.                 return null;
  1585.             }
  1586.             // 3.6.2. To-do Component
  1587.             // A "VTODO" calendar component without the "DTSTART" and "DUE" (or
  1588.             // "DURATION") properties specifies a to-do that will be associated
  1589.             // with each successive calendar date, until it is completed.
  1590.             var completedDate = item.completedDate;
  1591.             if (completedDate) {
  1592.                 var queryStart = ensureDateTime(rangeStart);
  1593.                 completedDate = ensureDateTime(completedDate);
  1594.                 return (!queryStart || completedDate.compare(queryStart) > 0);
  1595.             }
  1596.             return true;
  1597.         }
  1598.         endDate = (dueDate || startDate);
  1599.     }
  1600.  
  1601.     var start = ensureDateTime(startDate);
  1602.     var end = ensureDateTime(endDate);
  1603.  
  1604.     var queryStart = ensureDateTime(rangeStart);
  1605.     var queryEnd = ensureDateTime(rangeEnd);
  1606.  
  1607.     if (start.compare(end) == 0) {
  1608.         if ((!queryStart || start.compare(queryStart) >= 0) &&
  1609.             (!queryEnd || start.compare(queryEnd) < 0)) {
  1610.             return startDate;
  1611.         }
  1612.     } else {
  1613.         if ((!queryEnd || start.compare(queryEnd) < 0) &&
  1614.             (!queryStart || end.compare(queryStart) > 0)) {
  1615.             return startDate;
  1616.         }
  1617.     }
  1618.     return null;
  1619. }
  1620.  
  1621. /**
  1622.  * Returns true if we are Sunbird (according to our UUID), false otherwise.
  1623.  */
  1624. function isSunbird()
  1625. {
  1626.     const kSUNBIRD_UID = "{718e30fb-e89b-41dd-9da7-e25a45638b28}";
  1627.     var appInfo = Components.classes["@mozilla.org/xre/app-info;1"].
  1628.                   getService(Components.interfaces.nsIXULAppInfo);
  1629.  
  1630.     return appInfo.ID == kSUNBIRD_UID;
  1631. }
  1632.  
  1633.  
  1634.  
  1635. function hasPositiveIntegerValue(elementId)
  1636. {
  1637.     var value = document.getElementById(elementId).value;
  1638.     if (value && (parseInt(value) == value) && value > 0) {
  1639.         return true;
  1640.     }
  1641.     return false;
  1642. }
  1643.  
  1644. function getAtomFromService(aStr) {
  1645.     var atomService = Components.classes["@mozilla.org/atom-service;1"]
  1646.                       .getService(Components.interfaces.nsIAtomService);
  1647.     return atomService.getAtom(aStr);
  1648. }
  1649.  
  1650. function calInterfaceBag(iid) {
  1651.     this.init(iid);
  1652. }
  1653. calInterfaceBag.prototype = {
  1654.     mIid: null,
  1655.     mInterfaces: null,
  1656.  
  1657.     /// internal:
  1658.     init: function calInterfaceBag_init(iid) {
  1659.         this.mIid = iid;
  1660.         this.mInterfaces = [];
  1661.     },
  1662.  
  1663.     /// external:
  1664.     get size() {
  1665.         return this.mInterfaces.length;
  1666.     },
  1667.  
  1668.     get interfaceArray() {
  1669.         return this.mInterfaces;
  1670.     },
  1671.  
  1672.     add: function calInterfaceBag_add(iface) {
  1673.         if (iface) {
  1674.             var iid = this.mIid;
  1675.             function eq(obj) {
  1676.                 return compareObjects(obj, iface, iid);
  1677.             }
  1678.             if (!this.mInterfaces.some(eq)) {
  1679.                 this.mInterfaces.push(iface);
  1680.             }
  1681.         }
  1682.     },
  1683.  
  1684.     remove: function calInterfaceBag_remove(iface) {
  1685.         if (iface) {
  1686.             var iid = this.mIid;
  1687.             function neq(obj) {
  1688.                 return !compareObjects(obj, iface, iid);
  1689.             }
  1690.             this.mInterfaces = this.mInterfaces.filter(neq);
  1691.         }
  1692.     },
  1693.  
  1694.     forEach: function calInterfaceBag_forEach(func) {
  1695.         this.mInterfaces.forEach(func);
  1696.     }
  1697. };
  1698.  
  1699. function calListenerBag(iid) {
  1700.     this.init(iid);
  1701. }
  1702. calListenerBag.prototype = {
  1703.     __proto__: calInterfaceBag.prototype,
  1704.  
  1705.     notify: function calListenerBag_notify(func, args) {
  1706.         function notifyFunc(iface) {
  1707.             try {
  1708.                 iface[func].apply(iface, args ? args : []);
  1709.             }
  1710.             catch (exc) {
  1711.                 Components.utils.reportError(exc + " STACK: " + STACK());
  1712.             }
  1713.         }
  1714.         this.mInterfaces.forEach(notifyFunc);
  1715.     }
  1716. };
  1717.  
  1718. function sendMailTo(aRecipient, aSubject, aBody) {
  1719.  
  1720.     if (Components.classes["@mozilla.org/messengercompose;1"]) {
  1721.         // We are in Thunderbird, we can use the compose interface directly
  1722.         var msgComposeService = Components.classes["@mozilla.org/messengercompose;1"]
  1723.                                 .getService(Components.interfaces.nsIMsgComposeService);
  1724.         var msgParams = Components.classes["@mozilla.org/messengercompose/composeparams;1"]
  1725.                         .createInstance(Components.interfaces.nsIMsgComposeParams);
  1726.         var composeFields = Components.classes["@mozilla.org/messengercompose/composefields;1"]
  1727.                             .createInstance(Components.interfaces.nsIMsgCompFields);
  1728.  
  1729.         composeFields.to = aRecipient;
  1730.         composeFields.subject = aSubject;
  1731.         composeFields.body = aBody;
  1732.  
  1733.         msgParams.type = Components.interfaces.nsIMsgCompType.New;
  1734.         msgParams.format = Components.interfaces.nsIMsgCompFormat.Default;
  1735.         msgParams.composeFields = composeFields;
  1736.  
  1737.         msgComposeService.OpenComposeWindowWithParams(null, msgParams);
  1738.     } else {
  1739.         // We are in a place without a composer. Use the external protocol
  1740.         // service.
  1741.         var protoSvc = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"]
  1742.                        .getService(Components.interfaces.nsIExternalProtocolService);
  1743.         var ioService = Components.classes["@mozilla.org/network/io-service;1"]
  1744.                         .getService(Components.interfaces.nsIIOService);
  1745.  
  1746.         var uriString = "mailto:";
  1747.         var uriParams = [];
  1748.         if (aRecipient) {
  1749.             uriString += aRecipient;
  1750.         }
  1751.  
  1752.         if (aSubject) {
  1753.             uriParams.push("subject=" + encodeURIComponent(aSubject));
  1754.         }
  1755.  
  1756.         if (aBody) {
  1757.             uriParams.push("body=" + encodeURIComponent(aSubject));
  1758.         }
  1759.  
  1760.         if (uriParams.length > 0) {
  1761.             uriString += "?" + uriParams.join("&");
  1762.         }
  1763.  
  1764.         protoSvc.loadUrl(ioService.newURI(uriString, null, null));
  1765.     }
  1766. }
  1767.  
  1768. /**
  1769.  * This object implements calIOperation and could group multiple sub
  1770.  * operations into one. You can pass a cancel function which is called once
  1771.  * the operation group is cancelled.
  1772.  * Users must call notifyCompleted() once all sub operations have been
  1773.  * successful, else the operation group will stay pending.
  1774.  * The reason for the latter is that providers currently should (but need
  1775.  * not) implement (and return) calIOperation handles, thus there may be pending
  1776.  * calendar operations (without handle).
  1777.  */
  1778. function calOperationGroup(cancelFunc) {
  1779.     this.wrappedJSObject = this;
  1780.     if (calOperationGroup.mOpGroupId === undefined) {
  1781.         calOperationGroup.mOpGroupId = 0;
  1782.     }
  1783.     if (calOperationGroup.mOpGroupPrefix === undefined) {
  1784.         calOperationGroup.mOpGroupPrefix = (getUUID() + "-");
  1785.     }
  1786.     this.mCancelFunc = cancelFunc;
  1787.     this.mId = (calOperationGroup.mOpGroupPrefix + calOperationGroup.mOpGroupId++);
  1788.     this.mSubOperations = [];
  1789. }
  1790. calOperationGroup.prototype = {
  1791.     mCancelFunc: null,
  1792.     mId: null,
  1793.     mIsPending: true,
  1794.     mStatus: Components.results.NS_OK,
  1795.     mSubOperations: null,
  1796.  
  1797.     add: function calOperationGroup_add(op) {
  1798.         if (op && op.isPending) {
  1799.             this.mSubOperations.push(op);
  1800.         }
  1801.     },
  1802.  
  1803.     remove: function calOperationGroup_remove(op) {
  1804.         if (op) {
  1805.             function filterFunc(op_) {
  1806.                 return (op.id != op_.id);
  1807.             }
  1808.             this.mSubOperations = this.mSubOperations.filter(filterFunc);
  1809.         }
  1810.     },
  1811.  
  1812.     get isEmpty() {
  1813.         return (this.mSubOperations.length == 0);
  1814.     },
  1815.  
  1816.     notifyCompleted: function calOperationGroup_notifyCompleted(status) {
  1817.         ASSERT(this.isPending, "[calOperationGroup_notifyCompleted] this.isPending");
  1818.         if (this.isPending) {
  1819.             this.mIsPending = false;
  1820.             if (status) {
  1821.                 this.mStatus = status;
  1822.             }
  1823.         }
  1824.     },
  1825.  
  1826.     toString: function calOperationGroup_toString() {
  1827.         return ("[calOperationGroup] id=" + this.id);
  1828.     },
  1829.  
  1830.     // calIOperation:
  1831.     get id() {
  1832.         return this.mId;
  1833.     },
  1834.  
  1835.     get isPending() {
  1836.         return this.mIsPending;
  1837.     },
  1838.  
  1839.     get status() {
  1840.         return this.mStatus;
  1841.     },
  1842.  
  1843.     cancel: function calOperationGroup_cancel(status) {
  1844.         if (this.isPending) {
  1845.             if (!status) {
  1846.                 status = Components.interfaces.calIErrors.OPERATION_CANCELLED;
  1847.             }
  1848.             this.notifyCompleted(status);
  1849.             var cancelFunc = this.mCancelFunc;
  1850.             if (cancelFunc) {
  1851.                 this.mCancelFunc = null;
  1852.                 cancelFunc();
  1853.             }
  1854.             var subOperations = this.mSubOperations;
  1855.             this.mSubOperations = [];
  1856.             function forEachFunc(op) {
  1857.                 op.cancel(Components.interfaces.calIErrors.OPERATION_CANCELLED);
  1858.             }
  1859.             subOperations.forEach(forEachFunc);
  1860.         }
  1861.     }
  1862. };
  1863.  
  1864. function sameDay(date1, date2) {
  1865.     if (date1 && date2) {
  1866.         if ((date1.day == date2.day) &&
  1867.             (date1.month == date2.month) &&
  1868.             (date1.year == date2.year)) {
  1869.               return true;
  1870.         }
  1871.     }
  1872.     return false;
  1873. }
  1874.  
  1875. /**
  1876.  * Centralized funtions for accessing prodid and version
  1877.  */
  1878. function calGetProductId() {
  1879.     return "-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN";
  1880. }
  1881. function calGetProductVersion() {
  1882.     return "2.0";
  1883. }
  1884.  
  1885. /**
  1886.  * This is a centralized function for setting the prodid and version on an
  1887.  * ical component.  This should be used whenever you need to set the prodid
  1888.  * and version on a calIcalComponent object.
  1889.  *
  1890.  * @param
  1891.  *      aIcalComponent  The ical component to set the prodid and version on.
  1892.  */
  1893. function calSetProdidVersion(aIcalComponent) {
  1894.     // Throw for an invalid parameter
  1895.     if (!(aIcalComponent instanceof Components.interfaces.calIIcalComponent)) {
  1896.         throw Components.results.NS_ERROR_INVALID_ARG;
  1897.     }
  1898.     // Set the prodid and version
  1899.     aIcalComponent.prodid = calGetProductId();
  1900.     aIcalComponent.version = calGetProductVersion();
  1901. }
  1902.  
  1903. /**
  1904.  * This function returns a sibling of a XUL element, that is positioned behind
  1905.  * it in the DOM hierarchy *
  1906.  * @param
  1907.  *      aElement  The XUL element to derive the sibling from
  1908.  * @param
  1909.  *      aDistance  An integer value denoting how the relative position 
  1910.  *                  of the returned sibling within the parent container
  1911.  */
  1912. function getAdjacentSibling(aElement, aDistance) {
  1913.     var retElement = aElement;
  1914.     if (aDistance > 0) {
  1915.         for (var i = 0; i < aDistance; i++) {
  1916.             if (retElement) {
  1917.                 try {
  1918.                     retElement = retElement.nextSibling;
  1919.                 } catch (e) {
  1920.                     retElement = null;
  1921.                     i = aDistance;
  1922.                 }
  1923.             }
  1924.         }
  1925.     }
  1926.     return retElement;
  1927. }
  1928.  
  1929. /**
  1930.  * deeply clones a popupmenu
  1931.  *
  1932.  * @param aMenuPopupId The Id of the popup-menu to be cloned
  1933.  * @param aNewPopupId The new id of the cloned popup-menu
  1934.  * @param aNewIdPrefix To keep the ids unique the childnodes of the returned 
  1935.  * popup-menu are prepended with a prefix
  1936.  * @return the cloned popup-menu
  1937.  */
  1938. function clonePopupMenu(aMenuPopupId, aNewPopupId, aNewIdPrefix) {
  1939.     var oldMenuPopup = document.getElementById(aMenuPopupId);
  1940.     var retMenuPopup = oldMenuPopup.cloneNode(true);
  1941.     retMenuPopup.setAttribute("id", aNewPopupId);
  1942.     var menuElements = retMenuPopup.getElementsByAttribute("id", "*");
  1943.     for (var i = 0; i < menuElements.length; i++) {
  1944.         var lid = menuElements[i].getAttribute("id");
  1945.         menuElements[i].setAttribute("id", aNewIdPrefix + lid);
  1946.     }
  1947.     return retMenuPopup;
  1948. }
  1949.  
  1950. /**
  1951.  * applies a value to all children of a Menu. If the respective childnodes define
  1952.  * a command the value is applied to the attribute of thecommand of the childnode
  1953.  *
  1954.  * @param aElement The parentnode of the elements
  1955.  * @param aAttributeName The name of the attribute
  1956.  * @param aValue The value of the attribute
  1957.  */
  1958. function applyAttributeToMenuChildren(aElement, aAttributeName, aValue) {
  1959.    var sibling = aElement.firstChild;
  1960.    do {
  1961.        if (sibling) {
  1962.            var domObject = sibling;
  1963.            var commandName = null;
  1964.            if (sibling.hasAttribute("command")){
  1965.                commandName = sibling.getAttribute("command");
  1966.            }
  1967.            if (commandName) {
  1968.                var command = document.getElementById(commandName);
  1969.                if (command) {
  1970.                    domObject = command;
  1971.                }
  1972.            }
  1973.            domObject.setAttribute(aAttributeName, aValue);
  1974.        sibling = sibling.nextSibling;          
  1975.        }
  1976.     } while (sibling);
  1977.   }
  1978.  
  1979.  
  1980. /**
  1981.  * compares the value of a property of an array of objects and returns 
  1982.  * true or false if it is same or not among all array members 
  1983.  *
  1984.  * @param aObjects An Array of Objects to inspect
  1985.  * @param aProperty Name the name of the Property of which the value is compared
  1986.  */
  1987. function isPropertyValueSame(aObjects, aPropertyName) {
  1988.     var value = null;
  1989.     for (var i = 0; i < aObjects.length; i++) {
  1990.         if (!value) {
  1991.             value = aObjects[0][aPropertyName];
  1992.         }
  1993.         var compValue = aObjects[i][aPropertyName];
  1994.         if (compValue != value ) {
  1995.             return false;
  1996.         }
  1997.     }
  1998.     return true;
  1999. }
  2000.   
  2001. /**
  2002.  * sets the value of a boolean attribute by either setting the value or 
  2003.  * removing the attribute
  2004.  *
  2005.  * @param aXulElement The XulElement the attribute is applied to
  2006.  * @param aAttribute the name of the attribute
  2007.  * @param aValue the boolean value
  2008.  */
  2009. function setBooleanAttribute(aXulElement, aAttribute, aValue) {
  2010.     if (aXulElement) {
  2011.         if (aValue) {
  2012.             aXulElement.setAttribute(aAttribute, "true");
  2013.         }
  2014.         else {
  2015.             if (aXulElement.hasAttribute(aAttribute)) {
  2016.                 aXulElement.removeAttribute(aAttribute);
  2017.             }
  2018.         }
  2019.     }
  2020. }
  2021.  
  2022. function removeAnonymousElement(aParentNode, aId) {
  2023.     var child = document.getAnonymousElementByAttribute(aParentNode, "anonid", aId);
  2024.     child.parentNode.removeChild(child);
  2025. }
  2026.  
  2027. function getParentNode(aNode, aLocalName) {
  2028.   var node = aNode;
  2029.   do {
  2030.       node = node.parentNode;
  2031.   } while (node && (node.localName != aLocalName));
  2032.   return node;
  2033. }
  2034.  
  2035. function setItemProperty(item, propertyName, aValue, aCapability) {
  2036.     var isSupported = (item.calendar.getProperty("capabilities." + aCapability + ".supported") !== false)
  2037.     var value = (aCapability && !isSupported ? null : aValue);
  2038.  
  2039.     switch (propertyName) {
  2040.         case "startDate":
  2041.             if (value.isDate && !item.startDate.isDate ||
  2042.                 !value.isDate && item.startDate.isDate ||
  2043.                 !compareObjects(value.timezone, item.startDate.timezone) ||
  2044.                 value.compare(item.startDate) != 0) {
  2045.                 item.startDate = value;
  2046.             }
  2047.             break;
  2048.         case "endDate":
  2049.             if (value.isDate && !item.endDate.isDate ||
  2050.                 !value.isDate && item.endDate.isDate ||
  2051.                 !compareObjects(value.timezone, item.endDate.timezone) ||
  2052.                 value.compare(item.endDate) != 0) {
  2053.                 item.endDate = value;
  2054.             }
  2055.             break;
  2056.         case "entryDate":
  2057.             if (value == item.entryDate) {
  2058.                 break;
  2059.             }
  2060.             if (value && !item.entryDate ||
  2061.                 !value && item.entryDate ||
  2062.                 value.isDate != item.entryDate.isDate ||
  2063.                 !compareObjects(value.timezone, item.entryDate.timezone) ||
  2064.                 value.compare(item.entryDate) != 0) {
  2065.                 item.entryDate = value;
  2066.             }
  2067.             break;
  2068.         case "dueDate":
  2069.             if (value == item.dueDate) {
  2070.                 break;
  2071.             }
  2072.             if (value && !item.dueDate ||
  2073.                 !value && item.dueDate ||
  2074.                 value.isDate != item.dueDate.isDate ||
  2075.                 !compareObjects(value.timezone, item.dueDate.timezone) ||
  2076.                 value.compare(item.dueDate) != 0) {
  2077.                 item.dueDate = value;
  2078.             }
  2079.             break;
  2080.         case "isCompleted":
  2081.             if (value != item.isCompleted) {
  2082.                 item.isCompleted = value;
  2083.             }
  2084.             break;
  2085.         case "title":
  2086.             if (value != item.title) {
  2087.                 item.title = value;
  2088.             }
  2089.             break;
  2090.         default:
  2091.             if (!value || value == "") {
  2092.                 item.deleteProperty(propertyName);
  2093.             } else if (item.getProperty(propertyName) != value) {
  2094.                 item.setProperty(propertyName, value);
  2095.             }
  2096.             break;
  2097.     }
  2098. }
  2099.  
  2100.  
  2101. /**
  2102.  * Implements a property bag.
  2103.  */
  2104. function calPropertyBag() {
  2105.     this.mData = {};
  2106. }
  2107. calPropertyBag.prototype = {
  2108.     mData: null,
  2109.  
  2110.     setProperty: function cpb_setProperty(aName, aValue) {
  2111.         this.mData[aName] = aValue;
  2112.     },
  2113.     getProperty: function cpb_getProperty(aName) {
  2114.         var aValue = this.mData[aName];
  2115.         if (aValue === undefined) {
  2116.             aValue = null;
  2117.         }
  2118.         return aValue;
  2119.     },
  2120.     deleteProperty: function cpb_deleteProperty(aName) {
  2121.         delete this.mData[aName];
  2122.     },
  2123.     get enumerator() {
  2124.         return new calPropertyBagEnumerator(this);
  2125.     }
  2126. };
  2127. // implementation part of calPropertyBag
  2128. function calPropertyBagEnumerator(bag) {
  2129.     this.mIndex = 0;
  2130.     this.mBag = bag;
  2131.     var keys = [];
  2132.     for (var key in bag.mData) {
  2133.         keys.push(key);
  2134.     }
  2135.     this.mKeys = keys;
  2136. }
  2137. calPropertyBagEnumerator.prototype = {
  2138.     mIndex: 0,
  2139.     mBag: null,
  2140.     mKeys: null,
  2141.  
  2142.     // nsISimpleEnumerator:
  2143.     getNext: function cpb_enum_getNext() {
  2144.         if (!this.hasMoreElements()) { // hasMoreElements is called by intention to skip yet deleted properties
  2145.             ASSERT(false, Components.results.NS_ERROR_UNEXPECTED);
  2146.             throw Components.results.NS_ERROR_UNEXPECTED;
  2147.         }
  2148.         var name = this.mKeys[this.mIndex++];
  2149.         return { // nsIProperty:
  2150.             QueryInterface: function cpb_enum_prop_QueryInterface(aIID) {
  2151.                 ensureIID([Components.interfaces.nsIProperty, Components.interfaces.nsISupports], aIID);
  2152.                 return this;
  2153.             },
  2154.             name: name,
  2155.             value: this.mCurrentValue
  2156.         };
  2157.     },
  2158.     hasMoreElements: function cpb_enum_hasMoreElements() {
  2159.         while (this.mIndex < this.mKeys.length) {
  2160.             this.mCurrentValue = this.mBag.mData[this.mKeys[this.mIndex]];
  2161.             if (this.mCurrentValue !== undefined) {
  2162.                 return true;
  2163.             }
  2164.             ++this.mIndex;
  2165.         }
  2166.         return false;
  2167.     }
  2168. };
  2169.  
  2170. // Send iTIP invitation
  2171. function sendItipInvitation(aItem) {
  2172.     // XXX Until we rethink attendee support and until such support
  2173.     // is worked into the event dialog (which has been done in the prototype
  2174.     // dialog to a degree) then we are going to simply hack in some attendee
  2175.     // support so that we can round-trip iTIP invitations.
  2176.     // Since there is no way to determine the type of transport an
  2177.     // attendee requires, we default to email
  2178.     var emlSvc = Components.classes["@mozilla.org/calendar/itip-transport;1?type=email"]
  2179.                            .createInstance(Components.interfaces.calIItipTransport);
  2180.  
  2181.     var itipItem = Components.classes["@mozilla.org/calendar/itip-item;1"]
  2182.                              .createInstance(Components.interfaces.calIItipItem);
  2183.  
  2184.     var sbs = Components.classes["@mozilla.org/intl/stringbundle;1"]
  2185.                         .getService(Components.interfaces.nsIStringBundleService);
  2186.  
  2187.     var sb = sbs.createBundle("chrome://lightning/locale/lightning.properties");
  2188.     var recipients = [];
  2189.  
  2190.     // We have to modify our item a little, so we clone it.
  2191.     var item = aItem.clone();
  2192.  
  2193.     // Fix up our attendees for invitations using some good defaults
  2194.     itemAtt = item.getAttendees({}); // reuse cloned attendees
  2195.     item.removeAllAttendees();
  2196.     for each (var attendee in itemAtt) {
  2197.         attendee.role = "REQ-PARTICIPANT";
  2198.         attendee.participationStatus = "NEEDS-ACTION";
  2199.         attendee.rsvp = true;
  2200.         item.addAttendee(attendee);
  2201.         recipients.push(attendee);
  2202.     }
  2203.  
  2204.     // XXX The event dialog has no means to set us as the organizer
  2205.     // since we defaulted to email above, we know we need to prepend
  2206.     // mailto when we convert it to an attendee
  2207.     // This also means that when we are Updating an event, we will be making
  2208.     // a blatant assumption that you (the updater) are the organizer of the event.
  2209.     // This is probably ok since we don't support the iTIP COUNTER method,
  2210.     // but it would be better if we didn't allow you to modify an event that you
  2211.     // are not the organizer of and send out invitations to it as if you were.
  2212.     // For this support, we'll need a real invitation manager component.
  2213.     var organizer = Components.classes["@mozilla.org/calendar/attendee;1"]
  2214.                               .createInstance(Components.interfaces.calIAttendee);
  2215.     organizer.id = "mailto:" + emlSvc.defaultIdentity;
  2216.     organizer.role = "REQ-PARTICIPANT";
  2217.     organizer.participationStatus = "ACCEPTED";
  2218.     organizer.isOrganizer = true;
  2219.  
  2220.     // Add our organizer to the item. Again, the event dialog really doesn't
  2221.     // have a mechanism for creating an item with a method, so let's add
  2222.     // that too while we're at it.  We'll also fake Sequence ID support.
  2223.     item.organizer = organizer;
  2224.     item.setProperty("METHOD", "REQUEST");
  2225.     item.setProperty("SEQUENCE", item.generation);
  2226.  
  2227.     var summary
  2228.     if (item.getProperty("SUMMARY")) {
  2229.         summary = item.getProperty("SUMMARY");
  2230.     } else {
  2231.         summary = "";
  2232.     }
  2233.  
  2234.     // Initialize and set our properties on the item
  2235.     itipItem.init(item.icalString);
  2236.     itipItem.isSend = true;
  2237.     itipItem.receivedMethod = "REQUEST";
  2238.     itipItem.responseMethod = "REQUEST";
  2239.     itipItem.autoResponse = Components.interfaces.calIItipItem.USER;
  2240.  
  2241.     // Get ourselves some default text - when we handle organizer properly
  2242.     // We'll need a way to configure the Common Name attribute and we should
  2243.     // use it here rather than the email address
  2244.     var subject = sb.formatStringFromName("itipRequestSubject",
  2245.                                           [summary], 1);
  2246.     var body = sb.formatStringFromName("itipRequestBody",
  2247.                                        [emlSvc.defaultIdentity, summary],
  2248.                                        2);
  2249.  
  2250.     // Send it!
  2251.     emlSvc.sendItems(recipients.length, recipients, subject, body, itipItem);
  2252. }
  2253.